// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-12-17

package v2

import (
	"context"
	"gitee.com/go-libs/process/src"
	"gitee.com/go-libs/runtime"
	"github.com/google/uuid"
	"sync"
	"sync/atomic"
	"time"
)

var nilTime = time.Unix(0, 0)

// This is a process lifecycle manager.
type manager struct {
	mu        *sync.RWMutex
	name, uid string
	provider  src.Provider

	cancel       context.CancelFunc
	ctx          context.Context
	childMapper  map[string]Process
	childRunning int32
	parent       Process
	prevented    bool

	restarting                           bool
	started, starting, stopped, stopping bool
	startTime                            time.Time
}

// New creates a new process instance.
func New(name string, provider src.Provider) Process {
	return (&manager{
		name: name, uid: uuid.NewString(),
		provider: provider,
	}).init()
}

// + Implements.

func (o *manager) Add(children ...Process) Process      { return o.onAdd(children) }
func (o *manager) Del(children ...Process) Process      { return o.onDel(children) }
func (o *manager) GetChild(name string) (Process, bool) { return o.getChild(name) }
func (o *manager) GetName() string                      { return o.name }
func (o *manager) GetUid() string                       { return o.uid }
func (o *manager) Prevent(yes bool) Process             { return o.setPrevent(yes) }
func (o *manager) Restart()                             { o.onRestart() }
func (o *manager) Start(ctx context.Context) error      { return o.onStart(ctx) }
func (o *manager) Started() bool                        { return o.IsStarted() }
func (o *manager) StartedTime() time.Time               { return o.getStartedTime() }
func (o *manager) Starting() bool                       { return o.IsStarting() }
func (o *manager) Stop()                                { o.onStop() }
func (o *manager) Stopped() bool                        { return o.IsStopped() }
func (o *manager) Stopping() bool                       { return o.IsStopping() }
func (o *manager) StopWait()                            { o.onWait() }

// + Access methods.

func (o *manager) IsStarted() bool {
	o.mu.RLock()
	defer o.mu.RUnlock()
	return o.started
}

func (o *manager) IsStarting() (yes bool) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	return o.starting
}

func (o *manager) IsStopped() (yes bool) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	return o.stopped
}

func (o *manager) IsStopping() (yes bool) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	return o.stopping
}

// + Getter methods.

func (o *manager) getChild(name string) (child Process, exists bool) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	child, exists = o.childMapper[name]
	return
}

func (o *manager) getStartedTime() (tm time.Time) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	return o.startTime
}

// + Event methods.

func (o *manager) onAdd(children []Process) *manager {
	var (
		adds = 0
		ctx  context.Context
	)

	// Iterate children and compare with mapping.
	o.mu.Lock()
	for _, child := range children {
		// Skip nil process.
		if child == nil {
			continue
		}

		// Send stop signal to the process with the same name. It's reference
		// in mapper will be replaced.
		if p, ok := o.childMapper[child.GetName()]; ok {
			p.Stop()
		}

		// Bind parent to the child then update mapper.
		child.(*manager).parent = o
		o.childMapper[child.GetName()] = child

		// Add child to mapping.
		adds++
	}

	// Read context to start children.
	ctx = o.ctx
	o.mu.Unlock()

	// Start child process if current process started.
	if adds > 0 && ctx != nil && ctx.Err() == nil {
		o.onChildrenStart(ctx)
	}
	return o
}

func (o *manager) onChildrenStart(ctx context.Context) {
	o.mu.RLock()
	defer o.mu.RUnlock()

	// Iterate child processes in mapper. Start process in a goroutine if not
	// started or stopped.
	for _, c := range o.childMapper {
		if c.Stopped() {
			atomic.AddInt32(&o.childRunning, 1)
			go func(child Process) {
				defer atomic.AddInt32(&o.childRunning, -1)
				_ = child.Start(ctx)
			}(c)
		}
	}
}

func (o *manager) onChildrenStopped() {
	for {
		if atomic.LoadInt32(&o.childRunning) == 0 {
			return
		}
		time.Sleep(time.Millisecond)
	}
}

func (o *manager) onDel(children []Process) *manager {
	o.mu.Lock()
	defer o.mu.Unlock()

	// Iterate children and compare with mapping.
	for _, child := range children {
		// Skip nil process.
		if child == nil {
			continue
		}

		name := child.GetName()

		// Read process from mapped with name. Only deal with the same uuid. Skip
		// to delete if not same.
		if p, ok := o.childMapper[name]; ok && p.GetUid() == child.GetUid() {
			// Force to stop child process.
			child.Stop()

			// Remove from mapping.
			delete(o.childMapper, child.GetName())
		}
	}
	return o
}

func (o *manager) onHandle(ctx context.Context, hook func(context.Context) error) (err error) {
	// Invoked when handler ended.
	defer func() {
		// Recover and catch runtime panic.
		if r := recover(); r != nil {
			runtime.GetCounter().ProcessCounter().IncrFatalCount()

			// Set error reason if not specified by handler.
			if err == nil {
				err = src.ErrHookPanic
			}

			// Invoke panic.
			o.onPanic(ctx, r)
		}
	}()

	// Invoke handler.
	err = hook(ctx)
	return
}

func (o *manager) onPanic(ctx context.Context, v any) {
	// Discard runtime panic.
	defer func() {
		if r := recover(); r != nil {
			runtime.GetCounter().ProcessCounter().IncrFatalCount()
		}
	}()

	// Invoke handler.
	if hook, ok := o.provider.(src.ProviderPanic); ok {
		hook.Panic(ctx, v)
	}
	return
}

func (o *manager) onRestart() {
	o.mu.Lock()
	defer o.mu.Unlock()

	// Send signal if the process is running.
	if ctx := o.ctx; ctx != nil && ctx.Err() == nil {
		o.restarting, o.stopping = true, false
		o.cancel()
	}
}

func (o *manager) onStart(ctx context.Context) (err error) {
	o.mu.Lock()

	// Return error if the context is nil.
	if ctx == nil {
		o.mu.Unlock()
		err = src.ErrNilContext
		return
	}

	// Return error if the process provider is nil.
	if o.provider == nil {
		o.mu.Unlock()
		err = src.ErrNilProvider
		return
	}

	// Return error if middle state check failed. It means that the process not allow
	// to call Start() method before stopped.
	if o.started || o.starting || o.stopping {
		o.mu.Unlock()
		return
	}

	// Update state to begin startup.
	//
	// - restarting : [-]
	// - started    : [-]
	// - starting   : [+]
	// - stopped    : [-]
	// - stopping   : [-]
	o.startTime = time.Now()
	o.starting, o.stopped = true, false
	o.mu.Unlock()
	runtime.GetCounter().ProcessCounter().Add(o.uid, o.name, o.startTime)

	// Clean state when the process stopped.
	defer func() {
		// Lock process state.
		o.mu.Lock()

		// Notifier parent process to remove from mapper.
		if parent, prevented := o.parent, o.prevented; parent != nil && !prevented {
			o.mu.Unlock()
			parent.Del(o)

			// Redo lock state.
			o.mu.Lock()
		}

		// Revert state to initialized.
		o.setInitialState()
		o.mu.Unlock()
		runtime.GetCounter().ProcessCounter().Del(o.uid)
	}()

	// Loop until the process stopped.
	for {
		o.mu.Lock()

		// Return by parent context error.
		if ctx.Err() != nil {
			o.starting, o.stopping = false, true
			o.mu.Unlock()
			return
		}

		// Return by Stop() method called.
		if o.stopping {
			o.starting = false
			o.mu.Unlock()
			return
		}

		// Create process context and update state.
		o.ctx, o.cancel = context.WithCancel(ctx)
		o.started, o.starting = true, false

		// Invoke before hook, return if error occurred.
		if hook, ok := o.provider.(src.ProviderBefore); ok {
			if err = o.onHandle(o.ctx, hook.Before); err != nil {
				o.started, o.stopping = false, true
				o.mu.Unlock()
				return
			}
		}

		// Release lock state.
		o.mu.Unlock()

		// Start child processes in goroutines.
		o.onChildrenStart(o.ctx)

		// Invoke main hook.
		err = o.onHandle(o.ctx, o.provider.Run)

		// Cancel the process context to notifier child process to stop.
		if o.ctx.Err() == nil {
			o.cancel()
		}

		// Wait all child process stopped.
		o.onChildrenStopped()

		// Invoke after hook of the provider if no error occurred on run hook.
		if err == nil {
			if hook, ok := o.provider.(src.ProviderAfter); ok {
				err = o.onHandle(o.ctx, hook.After)
			}
		}

		// Update process state when main hook done.
		o.mu.Lock()
		o.ctx, o.cancel = nil, nil
		if o.restarting {
			o.restarting = false
			o.starting = true
		} else {
			o.stopping = true
		}
		o.mu.Unlock()
	}
}

func (o *manager) onStop() {
	o.mu.Lock()
	defer o.mu.Unlock()

	// Send signal if the process is running.
	if ctx := o.ctx; ctx != nil && ctx.Err() == nil {
		o.restarting, o.stopping = false, true
		o.cancel()
	}
}

func (o *manager) onWait() {
	// Force to send stop signal.
	o.onStop()

	// Return if this process and child processes are stopped.
	for {
		if o.IsStopped() {
			return
		}
		time.Sleep(time.Millisecond)
	}
}

// + Setter methods.

func (o *manager) setPrevent(prevented bool) *manager {
	o.mu.Lock()
	defer o.mu.Unlock()
	o.prevented = prevented
	return o
}

func (o *manager) setInitialState() {
	o.startTime = nilTime
	o.started, o.starting, o.stopping, o.restarting = false, false, false, false
	o.stopped = true
}

// + Constructor

func (o *manager) init() *manager {
	o.childMapper = make(map[string]Process)
	o.mu = new(sync.RWMutex)
	o.setInitialState()
	runtime.GetCounter().ProcessCounter().IncrCreatedCount()
	return o
}
